/*
    $Id$
    MAR another horizontal scroll action game
    Copyright (C) 2009 Japan Manoretimedia Lab.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include "graphics.h"
#include "util.h"
#include <d3dx9.h>
#include <assert.h>
#include <windows.h>

#define TEXTURE_NUM (4)
#define MODEL_NUM   (64)
#define SPRITE_NUM  (1024)
#define LINE_NUM    (1024)

struct Texture {
    int                id;
    bool               use;
    LPDIRECT3DTEXTURE9 texture;
};

struct Model {
    int     id;
    bool    use;
    Texture *texture;
    XY      uvXY; ///< texture upper left UV
    WH      uvWH; ///< texture uv Width, Height
    WH      wh;   ///< actual Width, Height
};

struct Sprite {
    int      id;
    bool     use;
    Model    *model;
    XY       pos;
    float    z;
    uint32_t color;
};

struct Line {
    int      id;
    bool     use;
    XY       pos0;
    XY       pos1;
    float    z;
    uint32_t color0;
    uint32_t color1;
};

struct Graphics {
    LPDIRECT3D9             d3d;
    LPDIRECT3DDEVICE9       d3dDevice;
    LPDIRECT3DVERTEXBUFFER9 vb;
    WH                      clientWH;
    Texture                 textures[TEXTURE_NUM];
    Model                   models[MODEL_NUM];
    Sprite                  sprites[SPRITE_NUM];
    Line                    lines[LINE_NUM];
};

Graphics *
GR_Instance(void)
{
    static Graphics self;
    return &self;
}

///////////////////////////////////////////////////////////////////////////

static Texture *
TextureInstance(Graphics *self, int n)
{
    if (n < 0 || TEXTURE_NUM <= n) {
        return NULL;
    }
    return &self->textures[n];
}

static void
InitTextures(Graphics *self)
{
    for (int i=0; i<TEXTURE_NUM; ++i) {
        Texture *p = TextureInstance(self, i);
        p->id = i;
        p->use = false;
    }
}

static Texture *
FindUnusedTexture(Graphics *self)
{
    for (int i=0; i<TEXTURE_NUM; ++i) {
        Texture *p = TextureInstance(self, i);
        if (!p->use) {
            p->use = true;
            return p;
        }
    }
    return NULL;
}

int
GR_CreateTextureFromFile(
    Graphics *self,
    const wchar_t *textureFileName)
{
    Texture *p = FindUnusedTexture(self);
    if (NULL == p) {
        return -1;
    }

    assert(self);
    assert(self->d3dDevice);

    if (FAILED(D3DXCreateTextureFromFile(
        self->d3dDevice, textureFileName, &p->texture))) {
        wchar_t s[FILE_PATH_NUM];
        wcscpy_s(s, sizeof s/sizeof s[0] -1, L"../");
        wcscat_s(s, sizeof s/sizeof s[0] -1, textureFileName);

        if (FAILED(D3DXCreateTextureFromFile(self->d3dDevice, s, &p->texture))) {
            wcscpy_s(s, sizeof s/sizeof s[0] -1, L"Could not find ");
            wcscat_s(s, sizeof s/sizeof s[0] -1, textureFileName);
            MessageBox( NULL, s, Title(), MB_OK);
            p->use = false;
            return -1;
        }
    }

    return p->id;
}

int
GR_CreateTextureFromFileS(
    Graphics *self,
    const char *textureFileName)
{
    wchar_t s[FILE_PATH_NUM];
    ZeroMemory(s, sizeof s);

    MultiByteToWideChar(CP_ACP, 0, textureFileName, -1, s, sizeof s/sizeof s[0]-1);
    return GR_CreateTextureFromFile(self, s);
}

void
GR_ReleaseTexture(
    Graphics *self,
    int textureId)
{
    Texture *p = TextureInstance(self, textureId);
    if (NULL == p) {
        return;
    }

    SAFE_RELEASE(p->texture);
    p->use = false;
}

static void
ReleaseAllTextures(Graphics *self)
{
    for (int i=0; i<TEXTURE_NUM; ++i) {
        GR_ReleaseTexture(self, i);
    }
}

///////////////////////////////////////////////////////////////////////////

static Model *
ModelInstance(Graphics *self, int n)
{
    if (n < 0 || MODEL_NUM <= n) {
        return NULL;
    }
    return &self->models[n];
}

static void
InitModels(Graphics *self)
{
    for (int i=0; i<MODEL_NUM; ++i) {
        Model *p = ModelInstance(self, i);
        p->id = i;
        p->use = false;
    }
}

static Model *
FindUnusedModel(Graphics *self)
{
    for (int i=0; i<MODEL_NUM; ++i) {
        Model *p = ModelInstance(self, i);
        if (!p->use) {
            p->use = true;
            return p;
        }
    }
    return NULL;
}

/** @return modelId
 */
int
GR_RegisterModel(
    Graphics *self,
    int textureId,
    const WH &wh,
    const XY &uvXY,
    const WH &uvWH)
{
    Model *p = FindUnusedModel(self);
    if (NULL == p) {
        return -1;
    }

    p->texture = TextureInstance(self, textureId);
    assert(p->texture);
    p->wh   = wh;
    p->uvXY = uvXY;
    p->uvWH = uvWH;

    return p->id;
}

int
GR_RegisterModelS(
    Graphics *self,
    int textureId,
    float width,
    float height,
    float uvX,
    float uvY,
    float uvW,
    float uvH)
{
    return GR_RegisterModel(self, textureId, WH(width, height), XY(uvX, uvY), WH(uvW, uvH));
}

void
GR_UnregisterModel(Graphics *self, int modelId)
{
    Model *p = ModelInstance(self, modelId);
    if (NULL == p) {
        return;
    }
    p->use = false;
}

///////////////////////////////////////////////////////////////////////////

static Sprite *
SpriteInstance(Graphics *self, int n)
{
    if (n < 0 || SPRITE_NUM <= n) {
        return NULL;
    }
    return &self->sprites[n];
}

static void
InitSprites(Graphics *self)
{
    for (int i=0; i<SPRITE_NUM; ++i) {
        Sprite *p = SpriteInstance(self, i);
        p->id = i;
        p->use = false;
    }
}

static Sprite *
FindUnusedSprite(Graphics *self)
{
    for (int i=0; i<SPRITE_NUM; ++i) {
        Sprite *p = SpriteInstance(self, i);
        if (!p->use) {
            p->use = true;
            return p;
        }
    }
    return NULL;
}

static void
ClearAllSprites(Graphics *self)
{
    for (int i=0; i<SPRITE_NUM; ++i) {
        Sprite *p = SpriteInstance(self, i);
        p->use = false;
    }
}

void
GR_PutSprite(
    Graphics *self,
    int modelId,
    const XY &pos,
    float z,
    uint32_t color)
{
    Sprite *p = FindUnusedSprite(self);
    if (NULL == p) {
        assert(0);
        return;
    }

    p->model = ModelInstance(self, modelId);
    assert(p->model);
    p->pos   = pos;

    // pos.y is bottom position upVector but p->pos.y is top position downVector
    p->pos.y = self->clientWH.h - pos.y - p->model->wh.h;

    // pixel offset from texel
    p->pos.x -= 0.5f;
    p->pos.y -= 0.5f;

    p->z     = z;
    p->color = color;
}

void
GR_PutSpriteS(
    Graphics *self,
    int modelId,
    float posX,
    float posY,
    float z,
    uint32_t color)
{
    GR_PutSprite(self, modelId, XY(posX, posY), z, color);
}

///////////////////////////////////////////////////////////////////////////

static Line *
LineInstance(Graphics *self, int n)
{
    if (n < 0 || LINE_NUM <= n) {
        return NULL;
    }
    return &self->lines[n];
}

static void
InitLines(Graphics *self)
{
    for (int i=0; i<LINE_NUM; ++i) {
        Line *p = LineInstance(self, i);
        p->id = i;
        p->use = false;
    }
}

static Line *
FindUnusedLine(Graphics *self)
{
    for (int i=0; i<LINE_NUM; ++i) {
        Line *p = LineInstance(self, i);
        if (!p->use) {
            p->use = true;
            return p;
        }
    }
    return NULL;
}

static void
ClearAllLines(Graphics *self)
{
    for (int i=0; i<LINE_NUM; ++i) {
        Line *p = LineInstance(self, i);
        p->use = false;
    }
}

void
GR_DrawLine(Graphics *self, const XY &pos0, const XY &pos1, float z, uint32_t color0, uint32_t color1)
{
    Line *p = FindUnusedLine(self);
    if (NULL == p) {
        assert(0);
        return;
    }

    // pos.y is bottom position upVector but p->pos.y is top position downVector
    p->pos0   = pos0;
    p->pos0.y = self->clientWH.h - pos0.y;
    p->pos1   = pos1;
    p->pos1.y = self->clientWH.h - pos1.y;
    p->z      = z;
    p->color0 = color0;
    p->color1 = color1;
}

void
GR_DrawLineS(Graphics *self, float pos0X, float pos0Y, float pos1X, float pos1Y, float z, uint32_t color0, uint32_t color1)
{
    GR_DrawLine(self, XY(pos0X, pos0Y), XY(pos1X, pos1Y), z, color0, color1);
}

void
GR_DrawRect(Graphics *self, const XY &pos, const WH &wh, float z, uint32_t color)
{
    GR_DrawLine(self, pos, XY(pos.x+wh.w, pos.y), z, color, color);
    GR_DrawLine(self, pos, XY(pos.x, pos.y+wh.h), z, color, color);
    GR_DrawLine(self, XY(pos.x+wh.w, pos.y+wh.h), XY(pos.x+wh.w, pos.y), z, color, color);
    GR_DrawLine(self, XY(pos.x+wh.w, pos.y+wh.h), XY(pos.x, pos.y+wh.h), z, color, color);
}

///////////////////////////////////////////////////////////////////////////

struct CUSTOMVERTEX_PCT {
    FLOAT x, y, z, rhw;
    DWORD color;
    FLOAT tu, tv;
};

#define D3DFVF_CUSTOMVERTEX_PCT (D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX1)

static void
CustomVertexPctSet(
    CUSTOMVERTEX_PCT *pTo,
    float x,
    float y,
    float z,
    float rhw,
    uint32_t color,
    float u,
    float v)
{
    pTo->x = x;
    pTo->y = y;
    pTo->z = z;
    pTo->rhw = rhw;
    pTo->color = color;
    pTo->tu = u;
    pTo->tv = v;
}

struct CUSTOMVERTEX_PC {
    FLOAT x, y, z, rhw;
    DWORD color;
};

#define D3DFVF_CUSTOMVERTEX_PC (D3DFVF_XYZRHW|D3DFVF_DIFFUSE)

static void
CustomVertexPcSet(
    CUSTOMVERTEX_PC *pTo,
    float x,
    float y,
    float z,
    float rhw,
    uint32_t color)
{
    pTo->x = x;
    pTo->y = y;
    pTo->z = z;
    pTo->rhw = rhw;
    pTo->color = color;
}

static HRESULT
GR_InitD3D(Graphics *self, HWND hWnd)
{
    HRESULT hr = S_OK;

    assert(self);

    if (!self->d3d) {
        self->d3d = Direct3DCreate9(D3D_SDK_VERSION);
        if (NULL == self->d3d) {
            return E_FAIL;
        }
    }

    if (!self->d3dDevice) {
        D3DPRESENT_PARAMETERS d3dpp;
        ZeroMemory(&d3dpp, sizeof d3dpp);
        d3dpp.Windowed         = TRUE;
        d3dpp.SwapEffect       = D3DSWAPEFFECT_DISCARD;
        d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
        d3dpp.EnableAutoDepthStencil = TRUE;
        d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

        IFR(self->d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
            hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &self->d3dDevice));
        assert(self->d3dDevice);

        IFR(self->d3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE));
        IFR(self->d3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE));
        IFR(self->d3dDevice->SetRenderState(D3DRS_ZENABLE, TRUE));
        IFR(self->d3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE));

        IFR(self->d3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE));
        IFR(self->d3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA));
        IFR(self->d3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA));
    }

    return hr;
}

static HRESULT
GR_InitGeometry(Graphics *self)
{
    HRESULT hr;

    assert(self);

    IFR(self->d3dDevice->CreateVertexBuffer(
        sizeof(CUSTOMVERTEX_PCT) * SPRITE_NUM * 4, 0, D3DFVF_CUSTOMVERTEX_PCT,
        D3DPOOL_DEFAULT, &self->vb, NULL));

    return hr;
}

HRESULT
GR_Init(Graphics *self, HWND hWnd, const WH &clientWH)
{
    HRESULT hr;

    assert(self);

    self->clientWH = clientWH;

    InitTextures(self);
    InitModels(self);
    InitSprites(self);
    InitLines(self);

    IFR(GR_InitD3D(self, hWnd));
    IFR(GR_InitGeometry(self));

    return hr;
}

void
GR_Term(Graphics *self)
{
    ReleaseAllTextures(self);

    SAFE_RELEASE(self->vb)
    SAFE_RELEASE(self->d3dDevice)
    SAFE_RELEASE(self->d3d)
}

static HRESULT
CallDrawTriangleFan(Graphics *self, CUSTOMVERTEX_PCT *vertices, UINT nVertex)
{
    HRESULT hr;

    if (0 == nVertex) {
        return S_OK;
    }

    assert(nVertex %4 == 0);

    UINT lengthBytes = sizeof(CUSTOMVERTEX_PCT) * nVertex;
    void *pVertices = NULL;

    IFR(self->vb->Lock(0, lengthBytes, &pVertices, 0));

    memcpy(pVertices, vertices, lengthBytes);
    IFR(self->vb->Unlock());

    IFR(self->d3dDevice->SetStreamSource(0, self->vb, 0, sizeof(CUSTOMVERTEX_PCT)));
    IFR(self->d3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX_PCT));
    for (UINT i=0; i<nVertex/4; ++i) {
        IFR(self->d3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, i*4, 2));
    }
    return hr;
}

static HRESULT
CallDrawLineList(Graphics *self, CUSTOMVERTEX_PC *vertices, UINT nVertex)
{
    HRESULT hr;

    if (0 == nVertex) {
        return S_OK;
    }

    assert(nVertex %2 == 0);

    UINT lengthBytes = sizeof(CUSTOMVERTEX_PC) * nVertex;
    void *pVertices = NULL;

    IFR(self->vb->Lock(0, lengthBytes, &pVertices, 0));

    memcpy(pVertices, vertices, lengthBytes);
    IFR(self->vb->Unlock());

    IFR(self->d3dDevice->SetStreamSource(0, self->vb, 0, sizeof(CUSTOMVERTEX_PC)));
    IFR(self->d3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX_PC));
    IFR(self->d3dDevice->DrawPrimitive(D3DPT_LINELIST, 0, nVertex/2));
    return hr;
}

HRESULT
GR_RenderSprites(Graphics *self)
{
    HRESULT hr;

    IFR(self->d3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE));
    IFR(self->d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE));
    IFR(self->d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE));

    Texture *prevTexture = NULL;

    CUSTOMVERTEX_PCT vertices[SPRITE_NUM * 4];
    UINT nVertex = 0;

    for (int i=0; i<SPRITE_NUM; ++i) {
        Sprite *s = SpriteInstance(self, i);
        if (!s->use) {
            continue;
        }
        Model *m = s->model;
        assert(m);
        Texture *texture = m->texture;
        assert(texture);

        if (texture != prevTexture) {
            IFR(self->d3dDevice->SetTexture(0, texture->texture));
            IFR(CallDrawTriangleFan(self, vertices, nVertex));
            nVertex = 0;
            prevTexture = texture;
        }

        CustomVertexPctSet(
            &vertices[nVertex++], s->pos.x,           s->pos.y,
            s->z, 1.0f, s->color, m->uvXY.x,           m->uvXY.y);
        CustomVertexPctSet(
            &vertices[nVertex++], s->pos.x + m->wh.w, s->pos.y,
            s->z, 1.0f, s->color, m->uvXY.x+m->uvWH.w, m->uvXY.y);
        CustomVertexPctSet(
            &vertices[nVertex++], s->pos.x + m->wh.w, s->pos.y + m->wh.h,
            s->z, 1.0f, s->color, m->uvXY.x+m->uvWH.w, m->uvXY.y+m->uvWH.h);
        CustomVertexPctSet(
            &vertices[nVertex++], s->pos.x,           s->pos.y + m->wh.h,
            s->z, 1.0f, s->color, m->uvXY.x,           m->uvXY.y+m->uvWH.h);
    }

    IFR(CallDrawTriangleFan(self, vertices, nVertex));

    return hr;
}

HRESULT
GR_RenderLines(Graphics *self)
{
    HRESULT hr;
    CUSTOMVERTEX_PC vertices[LINE_NUM * 2];
    UINT nVertex = 0;

    for (int i=0; i<LINE_NUM; ++i) {
        Line *l = LineInstance(self, i);
        if (!l->use) {
            continue;
        }
        CustomVertexPcSet(
            &vertices[nVertex++], l->pos0.x, l->pos0.y,
            l->z, 1.0f, l->color0);
        CustomVertexPcSet(
            &vertices[nVertex++], l->pos1.x, l->pos1.y,
            l->z, 1.0f, l->color1);
    }

    IFR(self->d3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE));
    IFR(self->d3dDevice->SetTexture(0, NULL));
    IFR(CallDrawLineList(self, vertices, nVertex));

    return hr;
}

HRESULT
GR_Render(Graphics *self)
{
    HRESULT hr;

    assert(self);
    assert(self->d3dDevice);

    IFR(self->d3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
        D3DCOLOR_XRGB( 76, 119, 114 ), 1.0f, 0));

    IFR(self->d3dDevice->BeginScene());

    IFR(GR_RenderSprites(self));
    IFR(GR_RenderLines(self));

    IFR(self->d3dDevice->EndScene());

    IFR(self->d3dDevice->Present(NULL, NULL, NULL, NULL));

    ClearAllSprites(self);
    ClearAllLines(self);

    return hr;
}


